﻿using System;
using System.Collections.Generic;
using System.Text;
using Evaluant.Uss.DomainModel.ClassModel;
using System.CodeDom;
using System.CodeDom.Compiler;

namespace DomainCodeGenerator
{
    public class DomainCodeGenerator
    {
        #region Enum

        public enum GenerationOptions {SingleCodeFile, MultipleCodeFiles};
        public enum CodeLanguage { CSharp, VisualBasic };

        #endregion

        #region Members

        protected CodeNamespace currentCodeNamespace;
        protected CodeConstructor currentCodeConstructor;

        #endregion

        #region Ctor

        public DomainCodeGenerator()
        {
            this.outputPath = string.Empty;
            this.options = GenerationOptions.SingleCodeFile;
            this.language = CodeLanguage.CSharp;
        }

        public DomainCodeGenerator(Model domainModel) : this()
        {
            this.domainModel = domainModel;
        }

        #endregion

        #region Properties

        private Model domainModel;
        public Model DomainModel
        {
            get { return domainModel; }
            set { domainModel = value; }
        }

        private GenerationOptions options;
        public GenerationOptions Options
        {
	        get { return options;}
	        set { options = value;}
        }

        private CodeLanguage language;
        public CodeLanguage Language
        {
            get { return language; }
            set { language = value; }
        }


        private string outputPath;
        public string OutputPath
        {
            get { return outputPath; }
            set { outputPath = value; }
        }

        private string[] generatedFiles;
        public string[] GeneratedFiles
        {
            get { return generatedFiles; }
            set { generatedFiles = value; }
        }

        #endregion

        #region Methods

            #region Generation

        protected CodeDomProvider GetCodeGenerator()
        {
            switch (this.language)
            {
                case CodeLanguage.CSharp:
                    Microsoft.CSharp.CSharpCodeProvider cscProvider = new Microsoft.CSharp.CSharpCodeProvider();
                    return cscProvider;

                case CodeLanguage.VisualBasic:
                    Microsoft.VisualBasic.VBCodeProvider cvbProvider = new Microsoft.VisualBasic.VBCodeProvider();
                    return cvbProvider;
            }
            return null;
        }

        public virtual void GenerateSourceCode()
        {
            // Create CodeDom
            List<FileDef> codeObjFiles = CreateCodeDom();

            // Instanciate code generator
            CodeDomProvider codeGen = this.GetCodeGenerator();
            CodeGeneratorOptions cop = new CodeGeneratorOptions();
            cop.BlankLinesBetweenMembers = true;
            cop.VerbatimOrder = true;
            cop.BracingStyle = "C";

            // Create code files
            List<string> files = new List<string>();
            foreach (FileDef fd in codeObjFiles)
            {
                string fileName = System.IO.Path.Combine(this.outputPath, System.IO.Path.ChangeExtension(fd.FileName, codeGen.FileExtension));
                using (System.IO.StreamWriter sw = new System.IO.StreamWriter(fileName))
                {
                    codeGen.GenerateCodeFromNamespace(fd.CodeObj as CodeNamespace, sw, cop);
                }
                files.Add(fileName);
            }
            this.generatedFiles = files.ToArray();
        }

        protected virtual List<FileDef> CreateCodeDom()
        {
            List<FileDef> codeObjFiles = new List<FileDef>();

            if (this.options == GenerationOptions.SingleCodeFile)
            {
                CodeNamespace cn = CreatePackage(this.DomainModel.Package);
                foreach (Class c in this.DomainModel.Package.Classes)
                {
                    cn.Types.Add(CreateClass(c));
                }

                codeObjFiles.Add(new FileDef(this.DomainModel.Package.Name, cn));
            }
            else
            {
                
                foreach (Class c in this.DomainModel.Package.Classes)
                {
                    CodeNamespace cn = CreatePackage(this.DomainModel.Package);
                    cn.Types.Add(CreateClass(c));
                    codeObjFiles.Add(new FileDef(c.Name, cn));
                }
            }

            return codeObjFiles;
        }

        #endregion

            #region Namespace / Usings

        protected virtual CodeNamespace CreatePackage(Package package)
        {
            currentCodeNamespace = new CodeNamespace(package.Name);
            currentCodeNamespace.Imports.Add(new CodeNamespaceImport("System"));
            return currentCodeNamespace;
        }

        protected virtual void AddImport(string ns)
        {
            bool alreadyExists = false;
            foreach (CodeNamespaceImport imp in currentCodeNamespace.Imports)
            {
                if (imp.Namespace == ns)
                {
                    alreadyExists = true;
                    break;
                }
            }

            if (!alreadyExists)
            {
                currentCodeNamespace.Imports.Add(new CodeNamespaceImport(ns));
            }
        }

        #endregion

            #region Class /Ctor

        protected virtual CodeTypeDeclaration CreateClass(Class c)
        {
            // create class
            CodeTypeDeclaration ctd = new CodeTypeDeclaration(c.Name);
            ctd.IsClass = true;
            ctd.Members.Add(CreateConstructor(c));

            // create properties
            foreach (Property p in c.Properties)
            {
                ctd.Members.Add(CreateField(p));
                ctd.Members.Add(CreateProperty(p));
            }

            // create references
            foreach (Relationship rel in this.domainModel.Package.Relationships)
            {
                if (rel.Parent.Name == c.Name)
                {
                    ctd.Members.Add(CreateField(rel.Child));
                    ctd.Members.Add(CreateProperty(rel.Child));
                }
            }

            return ctd;
        }

        protected virtual CodeConstructor CreateConstructor(Class c)
        {
            currentCodeConstructor = new CodeConstructor();
            currentCodeConstructor.Attributes = MemberAttributes.Public;
            return currentCodeConstructor;
        }

        #endregion

            #region Fields / Properties

        protected virtual CodeMemberField CreateField(Property prop)
        {
            // create field
            CodeMemberField cmf = new CodeMemberField(prop.Type, String.Concat("m_", prop.Name));
            cmf.Attributes = MemberAttributes.Family;

            return cmf;
        }
        
        protected virtual CodeMemberProperty CreateProperty(Property prop)
        {
            // create property
            CodeMemberProperty cmp = new CodeMemberProperty();
            cmp.Attributes = MemberAttributes.Public;
            cmp.Type = new CodeTypeReference(prop.Type);
            cmp.Name = prop.Name;
            cmp.HasGet = true;
            cmp.GetStatements.Add(new CodeSnippetExpression( String.Format("return m_{0}", prop.Name)));
            cmp.HasSet = true;
            cmp.SetStatements.Add(new CodeSnippetExpression(String.Format("m_{0} = value", prop.Name)));

            return cmp;
        }

        protected virtual CodeMemberField CreateField(Child child)
        {
            CodeTypeReference ctr = null;
            string fieldName = String.Concat("m_", child.Role); 

            // Define type
            if (child.Multiplicity == "*" || child.Multiplicity != "1")
            {
                AddImport("System.Collections.Generic");
                ctr = new CodeTypeReference("IList", new CodeTypeReference(child.Name));

                // Instantiate list in ctor
                CodeAssignStatement cas = new CodeAssignStatement();
                cas.Left = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), fieldName);
                cas.Right = new CodeObjectCreateExpression(new CodeTypeReference("List", new CodeTypeReference(child.Name)));
                currentCodeConstructor.Statements.Add(cas);
            }
            else
            {
                ctr = new CodeTypeReference(child.Name);
            }

            // create field
            CodeMemberField cmf = new CodeMemberField(ctr, fieldName);
            cmf.Attributes = MemberAttributes.Family;
            return cmf;
        }

        protected virtual CodeMemberProperty CreateProperty(Child child)
        {
            CodeTypeReference ctr = null;

            // Define type
            if (child.Multiplicity == "*" || child.Multiplicity != "1")
            {
                AddImport("System.Collections.Generic");
                ctr = new CodeTypeReference("IList", new CodeTypeReference(child.Name));
            }
            else
            {
                ctr = new CodeTypeReference(child.Name);
            }

            // create property
            CodeMemberProperty cmp = new CodeMemberProperty();
            cmp.Attributes = MemberAttributes.Public;
            cmp.Type = ctr;
            cmp.Name = child.Role;
            cmp.HasGet = true;
            cmp.GetStatements.Add(new CodeSnippetExpression(String.Format("return m_{0}", child.Role)));
            cmp.HasSet = true;
            cmp.SetStatements.Add(new CodeSnippetExpression(String.Format("m_{0} = value", child.Role)));

            return cmp;
        }

        #endregion

        #endregion

        #region FileDef Class

        protected class FileDef
        {
            #region Ctor

            public FileDef()
            {
            }

            public FileDef(string fileName, CodeObject codeObj)
            {
                this.fileName = fileName;
                this.codeObj = codeObj;
            }

            #endregion

            #region Properties

            private string fileName;
            public string FileName
            {
                get { return fileName; }
                set { fileName = value; }
            }

            private CodeObject codeObj;
            public CodeObject CodeObj
            {
                get { return codeObj; }
                set { codeObj = value; }
            }

            #endregion
        }

        #endregion
    }
}
